from typing import List, Dict, Any, Optional
from fastapi import UploadFile
from tortoise.queryset import QuerySet
from tortoise.exceptions import DoesNotExist
from src.models import DetectionRecord, User
from src.schemas.detection import (
    UploadImageResponse, 
    SaveImageResponse,
    DetectionResult,
    DetectionHistoryResponse,
    DetectionHistoryItem,
    ImageInfo,
    StepDetectionResponse,
    SingleImageResult,
    FinalSummaryResponse
)
from src.services.file_service import FileService
from src.utils.logger import app_logger as logger
from src.cores.exceptions import InvalidImageException
from src.cores.redis import redis_manager
# import httpx  # 已替换为OpenAI SDK
import oss2
import uuid
import time
import json
import logging
import asyncio
import random
import string
from datetime import datetime
from urllib.parse import urlparse
from src.config.settings import get_settings
from ..config.ai_config import AIDetectionConfig
from src.services.checkin_service import CheckinService
from openai import AsyncOpenAI


class DetectionService:
    """检测服务"""
    
    def __init__(self):
        self.settings = get_settings()
        # 会话存储使用Redis
        self.session_prefix = "detection_session:"
        self.session_expire_time = 24 * 60 * 60  # 24小时过期
        # 初始化 OpenAI 客户端
        self.openai_client = AsyncOpenAI(
            api_key=self.ai_api_key,
            base_url=self.ai_api_url
        )
    
    def _generate_session_id(self) -> str:
        """生成会话ID - 与前端格式保持一致"""
        timestamp = str(int(time.time() * 1000))  # 毫秒时间戳
        random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=9))
        return f"session_{timestamp}_{random_str}"

    # AI配置从环境变量读取
    @property
    def ai_api_url(self) -> str:
        return self.settings.AI_API_URL
    
    @property
    def ai_api_key(self) -> str:
        return self.settings.AI_API_KEY
    
    @property
    def ai_model(self) -> str:
        return self.settings.AI_MODEL
    
    @property
    def ai_timeout(self) -> int:
        return self.settings.AI_API_TIMEOUT
    
    @property
    def confidence_threshold(self) -> float:
        return self.settings.AI_CONFIDENCE_THRESHOLD
    
    async def upload_to_oss(self, user_id: int, image_file: UploadFile) -> UploadImageResponse:
        """仅上传图片到OSS，不保存数据库记录"""
        file_service = FileService()
        return await file_service.upload_to_oss(user_id, image_file)
    
    @staticmethod
    async def save_image_record(user_id: int, image_url: str, oss_path: str) -> SaveImageResponse:
        """保存图片信息到数据库"""
        try:
            # 创建检测记录（不包含检测结果）
            record = await DetectionRecord.create(
                user_id=user_id,
                image_url=image_url,
                oss_path=oss_path,
                result=None  # 暂时不包含检测结果
            )
            
            logger.info(f"用户 {user_id} 保存图片记录成功: 记录ID {record.id}")
            
            return SaveImageResponse(
                record_id=record.id,
                image_url=image_url,
                oss_path=oss_path,
                created_at=record.created_at
            )
            
        except Exception as e:
            logger.error(f"保存图片记录失败: {str(e)}")
            raise Exception(f"保存图片记录失败: {str(e)}")
    
    async def upload_image(self, user_id: int, image_file: UploadFile) -> UploadImageResponse:
        """上传图片到OSS（保持向后兼容）"""
        return await self.upload_to_oss(user_id, image_file)
    

    
    async def _call_ai_api_single_image(self, image_url: str, image_type: str) -> str:
        """调用AI API分析单张图片，带重试机制"""
        max_retries = 3
        retry_delay = 2  # 秒
        
        for attempt in range(max_retries):
            try:
                prompt = self._generate_single_image_prompt(image_type)
                
                # 使用OpenAI SDK调用API（流式输出）
                response = await self.openai_client.chat.completions.create(
                    model=self.ai_model,
                    messages=[
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "text",
                                    "text": prompt
                                },
                                {
                                    "type": "image_url",
                                    "image_url": {
                                        "url": image_url
                                    }
                                }
                            ]
                        }
                    ],
                    max_tokens=1000,
                    temperature=0.1,
                    timeout=self.ai_timeout,
                    stream=True  # 启用流式输出
                )
                
                # 处理流式响应
                ai_response = ""
                async for chunk in response:
                    # 安全检查：最后一个chunk的choices字段可能为空列表
                    if chunk.choices and chunk.choices[0].delta.content is not None:
                        ai_response += chunk.choices[0].delta.content
                logger.info(f"{image_type}照片AI分析响应: {ai_response}")
                return ai_response
                    
            except Exception as e:
                logger.warning(f"AI API调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt < max_retries - 1:
                    await asyncio.sleep(retry_delay * (attempt + 1))  # 递增延迟
                    continue
                else:
                    logger.error(f"AI API调用失败，已重试{max_retries}次")
                    raise Exception(f"AI分析失败: {str(e)}")

    def _generate_single_image_prompt(self, image_type: str) -> str:
        """生成单张图片分析的prompt"""
        from datetime import datetime
        from ..config.medical_knowledge import MedicalKnowledgeBase
        
        # 获取专业医学知识
        medical_knowledge = MedicalKnowledgeBase.generate_professional_prompt()
        
        # 根据图片类型定义分析重点
        angle_mapping = {
            'top': '头顶面',
            'side': '头侧面', 
            'back': '头后面'
        }
        
        angle_cn = angle_mapping.get(image_type, '头部')
        
        # 根据不同角度定义分析重点
        if image_type == 'top':
            focus_points = [
                "观察头顶部头发密度和分布情况",
                "检查发旋区域的头发稀疏程度",
                "评估头皮可见度和光泽度",
                "观察头顶部脱发的范围和形状",
                "评估头发直径和质量变化"
            ]
        elif image_type == 'side':
            focus_points = [
                "观察发际线的形状和后退程度",
                "检查M型脱发的发展情况",
                "评估太阳穴区域的头发密度",
                "观察侧面头发的层次和厚度",
                "检查发际线的清晰度和规整性"
            ]
        elif image_type == 'back':
            focus_points = [
                "观察后枕部头发密度作为参考标准",
                "检查后脑勺头发的健康状况",
                "评估后部头发的粗细和光泽",
                "观察后枕部是否有异常脱发",
                "作为其他区域脱发程度的对比基准"
            ]
        else:
            focus_points = [
                "观察整体头发密度和分布",
                "检查可见的脱发特征",
                "评估头皮健康状况",
                "观察头发质量和粗细变化"
            ]
        
        focus_text = "\n".join([f"   {i+1}. {point}" for i, point in enumerate(focus_points)])
        
        return f"""
你是一位专门研究脱发的医学专家，精通BASP分型法和雄激素性脱发诊断。现在请你分析这张{angle_cn}照片。

请基于以下专业医学知识进行分析：
{medical_knowledge}

针对{angle_cn}照片的分析重点：
{focus_text}

【重要要求】：
1. 首先判断图片是否包含头发、头皮或头部相关内容
2. 如果不包含相关内容，请返回无效结果
3. 如果包含相关内容，请详细描述观察到的特征
4. 重点关注{angle_cn}角度能够观察到的脱发特征
5. 提供这个角度的初步分析结果
6. 评估图片质量和可见特征的清晰程度

请严格按照以下JSON格式返回结果：
{{
    "isValid": true/false,
    "angle": "{angle_cn}",
    "description": "详细描述从{angle_cn}观察到的头发和头皮状况（至少80字）",
    "features": [
        "观察到的具体特征1",
        "观察到的具体特征2",
        "观察到的具体特征3"
    ],
    "preliminary_assessment": "基于{angle_cn}的初步评估",
    "image_quality": "图片质量评估（清晰/一般/模糊）",
    "confidence": 置信度数值（0-1之间的小数）
}}

分析时间戳：{datetime.now().isoformat()}
请确保返回有效的JSON格式，description必须详细且专业。
"""

    async def _call_ai_api_summary(self, analysis_results: List[str]) -> str:
        """调用AI API进行汇总分析，带重试机制"""
        max_retries = 3
        retry_delay = 2  # 秒
        
        for attempt in range(max_retries):
            try:
                prompt = self._generate_summary_prompt(analysis_results)
                
                response = await self.openai_client.chat.completions.create(
                    model=self.ai_model,
                    messages=[
                        {
                            "role": "user",
                            "content": prompt
                        }
                    ],
                    max_tokens=1500,
                    temperature=0.1,
                    timeout=self.ai_timeout,
                    stream=True  # 启用流式输出
                )
                
                # 处理流式响应
                ai_response = ""
                async for chunk in response:
                    # 安全检查：最后一个chunk的choices字段可能为空列表
                    if chunk.choices and chunk.choices[0].delta.content is not None:
                        ai_response += chunk.choices[0].delta.content
                logger.info(f"汇总分析AI响应: {ai_response}")
                return ai_response
                    
            except Exception as e:
                error_msg = str(e)
                
                # 检查是否为超时错误
                if "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
                    logger.warning(f"汇总分析AI API调用超时 (尝试 {attempt + 1}/{max_retries}): {error_msg}")
                    if attempt < max_retries - 1:
                        await asyncio.sleep(retry_delay * (attempt + 1))  # 递增延迟
                        continue
                    else:
                        logger.error(f"汇总分析AI API调用超时，已重试{max_retries}次")
                        raise Exception(f"汇总分析失败: 网络超时，请稍后重试")
                
                # 检查是否为服务器错误（可重试）
                elif any(code in error_msg for code in ["502", "503", "504", "500"]):
                    logger.warning(f"汇总分析AI API服务器错误 (尝试 {attempt + 1}/{max_retries}): {error_msg}")
                    if attempt < max_retries - 1:
                        await asyncio.sleep(retry_delay * (attempt + 1))
                        continue
                    else:
                        logger.error(f"汇总分析AI API服务器错误，已重试{max_retries}次")
                        raise Exception(f"汇总分析失败: 服务器暂时不可用，请稍后重试")
                
                # 其他错误（不重试）
                else:
                    logger.error(f"汇总分析AI API请求错误: {error_msg}")
                    raise Exception(f"汇总分析失败: 请求错误")

    def _generate_summary_prompt(self, analysis_results: List[str]) -> str:
        """生成汇总分析的prompt"""
        from datetime import datetime
        from ..config.medical_knowledge import MedicalKnowledgeBase
        
        # 获取专业医学知识
        medical_knowledge = MedicalKnowledgeBase.generate_professional_prompt()
        
        # 构建分析结果文本，包含角度信息
        results_text_parts = []
        angle_mapping = {'top': '头顶面', 'side': '头侧面', 'back': '头后面'}
        
        for i, result in enumerate(analysis_results):
            # 尝试从结果中提取角度信息，如果没有则使用默认
            angle_info = f"第{i+1}张照片"
            if i < len(analysis_results):
                if 'top' in str(result).lower() or '头顶' in str(result):
                    angle_info = "头顶面照片"
                elif 'side' in str(result).lower() or '侧面' in str(result) or '头侧' in str(result):
                    angle_info = "头侧面照片"
                elif 'back' in str(result).lower() or '后面' in str(result) or '头后' in str(result):
                    angle_info = "头后面照片"
            
            results_text_parts.append(f"【{angle_info}分析结果】：\n{result}")
        
        results_text = "\n\n".join(results_text_parts)
        
        # 添加结果验证信息
        results_count = len(analysis_results)
        validation_info = f"\n\n【重要提示】：以上共提供了{results_count}张照片的分析结果，请确保基于所有{results_count}张照片的信息进行综合分析。"
        
        return f"""
你是一位专门研究脱发的医学专家，精通BASP分型法和雄激素性脱发诊断。我已经分别分析了三张不同角度的头部照片，现在请你综合这些分析结果，给出最终的专业诊断。

以下是三张照片的分析结果：
{results_text}{validation_info}

请基于以下专业医学知识进行综合分析：
{medical_knowledge}

【重要要求】：
1. 必须基于上述所有提供的分析结果进行综合诊断，不得以"缺少分析结果"为由拒绝诊断
2. 即使某些分析结果格式不完整，也要尽力从中提取有用信息进行综合分析
3. 必须严格按照BASP分型标准确定分型（L0, M1-M3, C1-C3, U1-U3, V1-V3, F1-F3）
4. description必须详细描述（至少150字），包括：
   - 发际线形态特征（M型/C型/U型等）
   - 各个角度观察到的具体脱发情况
   - 头发密度变化的详细描述
   - 头皮可见度评估
   - BASP分型的具体依据
5. suggestion必须提供详细的护理建议（至少100字），包括：
   - 日常护理方法（洗发、护发）
   - 生活方式调整建议
   - 营养补充建议
   - 头皮护理要点
   - 预防进一步脱发的措施
   - 心理调适建议
6. 置信度要基于图片清晰度和特征明显程度合理评估
7. 如果某个角度的分析结果不够详细，请基于其他角度的信息进行合理推断

请严格按照以下JSON格式返回最终结果：
{{
    "isValid": true,
    "baspType": "具体BASP分型（必须是L0/M1/M2/M3/C1/C2/C3/U1/U2/U3/V1/V2/V3/F1/F2/F3之一）",
    "severity": "严重程度（正常、轻度、中度、重度、极重度）",
    "description": "详细的专业描述（至少150字），包含发际线形态、密度变化、各角度观察结果、BASP分型依据等",
    "suggestion": "详细的个性化护理建议（至少100字），包含日常护理、生活方式、营养补充、头皮护理、预防措施等，不推荐药物",
    "confidence": 置信度数值（0-1之间的小数）
}}

分析时间戳：{datetime.now().isoformat()}
请确保返回有效的JSON格式，description和suggestion必须详细且专业。
"""
    
    def _parse_ai_response(self, ai_response: str) -> Dict[str, Any]:
        """解析AI响应"""
        try:
            import json
            logger.debug(f"AI原始响应: {ai_response}")
            
            # 尝试解析JSON
            if '{' in ai_response and '}' in ai_response:
                start = ai_response.find('{')
                end = ai_response.rfind('}') + 1
                json_str = ai_response[start:end]
                parsed_result = json.loads(json_str)
                logger.debug(f"AI解析结果: {parsed_result}")
                
                # 检查图片是否有效
                is_valid = parsed_result.get("isValid", True)
                if not is_valid:
                    warning_msg = parsed_result.get("warning", "请上传包含头发或头皮的照片进行脱发检测")
                    logger.warning(f"图片内容无效: {warning_msg}")
                    # 返回包含警告信息的结果，而不是抛出异常
                    return {
                        "isValid": False,
                        "warning": warning_msg
                    }
                
                # 标准化字段名，确保使用正确的键名
                standardized_result = {
                    "baspType": parsed_result.get("baspType"),
                    "severity": parsed_result.get("severity"),
                    "description": parsed_result.get("description"),
                    "suggestion": parsed_result.get("suggestion") ,
                    "confidence": float(parsed_result.get("confidence"))
                }
                
                logger.debug(f"标准化结果: {standardized_result}")
                return standardized_result
            else:
                # 如果没有找到JSON格式，但响应不为空，说明AI返回了纯文本
                # 这种情况下，我们需要返回一个默认的结果结构
                logger.warning(f"AI返回非JSON格式响应，将作为描述处理: {ai_response[:200]}...")
                return {
                    "baspType": "无法确定",
                    "severity": "需要进一步检查",
                    "description": ai_response.strip(),
                    "suggestion": "建议提供更清晰的照片或咨询专业医生进行详细检查",
                    "confidence": 0.5
                }
                
        except json.JSONDecodeError as e:
            logger.error(f"JSON解析失败: {str(e)}, 原始响应: {ai_response}")
            
            # 检查是否是AI判别失误导致的解析失败
            if "缺少" in ai_response and ("照片" in ai_response or "分析" in ai_response):
                logger.warning(f"检测到AI判别失误，缺少照片分析内容: {ai_response[:200]}...")
                return {
                    "baspType": "检测失败",
                    "severity": "无法确定",
                    "description": "偶发的图片解析失败，请重新检测",
                    "suggestion": "请重新上传照片进行检测，确保照片清晰且包含完整的头发或头皮区域",
                    "confidence": 0.0
                }
            
            # 其他JSON解析失败情况
            return {
                "baspType": "解析失败",
                "severity": "无法确定",
                "description": f"AI响应解析失败，原始内容: {ai_response.strip()}",
                "suggestion": "请重新尝试检测或咨询专业医生",
                "confidence": 0.0
            }
        except Exception as e:
            logger.error(f"解析AI响应失败: {str(e)}, 原始响应: {ai_response}")
            # 其他异常时，返回默认结果
            return {
                "baspType": "系统错误",
                "severity": "无法确定",
                "description": f"系统处理异常: {str(e)}",
                "suggestion": "请稍后重试或联系技术支持",
                "confidence": 0.0
            }
    
    def _extract_oss_path(self, image_url: str) -> str:
        """从完整URL中提取OSS路径"""
        try:
            # 从URL中提取路径部分
            if self.settings.OSS_STORAGE_PATH in image_url:
                start = image_url.find(self.settings.OSS_STORAGE_PATH)
                return image_url[start:]
            return ""
        except:
            return ""
    
    @staticmethod
    async def get_detection_history(user_id: int, page: int = 1, limit: int = 10) -> DetectionHistoryResponse:
        """获取检测历史"""
        try:
            # 计算偏移量
            offset = (page - 1) * limit
            
            # 查询检测记录
            records = await DetectionRecord.filter(
                user_id=user_id
            ).order_by("-created_at").offset(offset).limit(limit)
            
            # 获取总数
            total = await DetectionRecord.filter(user_id=user_id).count()
            
            # 构造历史记录列表
            history_items = []
            for record in records:
                result = None
                if record.result:
                    result_dict = record.result_dict
                    result = DetectionResult(
                        basp_type=result_dict.get("baspType", ""),
                        severity=result_dict.get("severity", ""),
                        description=result_dict.get("description", ""),
                        suggestion=result_dict.get("suggestion", ""),
                        confidence=result_dict.get("confidence", 0.0),
                        is_valid=record.is_valid if hasattr(record, 'is_valid') else True
                    )
                
                # 构造图片信息列表
                images_info = []
                images_dict = record.images_dict
                file_service = FileService()
                
                for img_info in images_dict:
                    # 重新生成7天有效期的签名URL
                    original_url = img_info.get('image_url', '')
                    regenerated_url = file_service.regenerate_signed_url_from_url(original_url) if original_url else ''
                    
                    images_info.append(ImageInfo(
                        image_url=regenerated_url,
                        angle=img_info.get('angle', ''),
                        oss_path=img_info.get('oss_path', '')
                    ))
                
                # 为向后兼容，重新生成第一张图片的签名URL
                main_image_url = record.image_url
                if main_image_url:
                    main_image_url = file_service.regenerate_signed_url_from_url(main_image_url)
                
                history_items.append(DetectionHistoryItem(
                    id=record.id,
                    image_url=main_image_url,  # 向后兼容，返回第一张图片
                    images_info=images_info,  # 返回所有三个部位的照片
                    result=result,

                    created_at=record.created_at
                ))
            
            has_more = offset + limit < total
            
            logger.info(f"用户 {user_id} 查询检测历史: 第{page}页，共{total}条")
            
            return DetectionHistoryResponse(
                list=history_items,
                total=total,
                has_more=has_more
            )
            
        except Exception as e:
            logger.error(f"获取检测历史失败: {str(e)}")
            raise Exception(f"获取检测历史失败: {str(e)}")
    
    @staticmethod
    async def get_detection_record(user_id: int, record_id: int) -> dict:
        """获取检测记录详情"""
        try:
            record = await DetectionRecord.get(
                id=record_id,
                user_id=user_id
            )
            
            # 创建文件服务实例用于重新生成签名URL
            file_service = FileService()
            
            # 重新生成主图片URL的签名
            main_image_url = record.image_url
            if main_image_url:
                main_image_url = file_service.regenerate_signed_url_from_url(main_image_url)
            
            # 重新生成所有图片信息中的签名URL
            images_info = record.images_dict
            updated_images_info = []
            for img_info in images_info:
                updated_img_info = img_info.copy()
                original_url = img_info.get('image_url', '')
                if original_url:
                    updated_img_info['image_url'] = file_service.regenerate_signed_url_from_url(original_url)
                updated_images_info.append(updated_img_info)
            
            return {
                "id": record.id,
                "image_url": main_image_url,
                "images_info": updated_images_info,
                "detection_result": record.result_dict,
                "basp_type": record.basp_type,
                "severity": record.severity,
                "confidence": record.confidence,

                "created_at": record.created_at
            }
            
        except DoesNotExist:
            raise Exception("检测记录不存在")
        except Exception as e:
            logger.error(f"获取检测记录详情失败: {str(e)}")
            raise Exception(f"获取检测记录详情失败: {str(e)}")
    
    # 分步检测相关方法
    async def step_detection(self, user_id: int, session_id: Optional[str], image_info: Dict[str, Any]) -> StepDetectionResponse:
        """分步检测 - 单张图片检测"""
        try:
            logger.info(f"开始分步检测 - 用户ID: {user_id}, 传入的会话ID: {session_id}")
            
            # 角度映射
            angle_mapping = {
                'top': '头顶面',
                'side': '头侧面', 
                'back': '头后面'
            }
            
            required_angles = ['top', 'side', 'back']
            
            # 如果没有session_id，创建新会话
            if not session_id:
                session_id = self._generate_session_id()
                session_data = {
                    'user_id': user_id,
                    'created_at': datetime.now().isoformat(),
                    'completed_angles': [],
                    'results': [],
                    'images_info': []
                }
                # 存储到Redis
                session_key = f"{self.session_prefix}{session_id}"
                await redis_manager.set(session_key, json.dumps(session_data), ex=self.session_expire_time)
                logger.info(f"创建新检测会话: {session_id}，过期时间: {self.session_expire_time}秒，会话数据: {session_data}")
            
            # 验证会话
            session_key = f"{self.session_prefix}{session_id}"
            logger.debug(f"查找会话: {session_key}")
            session_data_str = await redis_manager.get(session_key)
            if not session_data_str:
                # 会话不存在或已过期，创建新会话
                logger.warning(f"会话ID {session_id} 不存在或已过期，创建新会话")
                session_id = self._generate_session_id()
                session_data = {
                    'user_id': user_id,
                    'created_at': datetime.now().isoformat(),
                    'completed_angles': [],
                    'results': [],
                    'images_info': []
                }
                # 存储到Redis
                session_key = f"{self.session_prefix}{session_id}"
                await redis_manager.set(session_key, json.dumps(session_data), ex=self.session_expire_time)
                logger.info(f"创建新检测会话: {session_id}，过期时间: {self.session_expire_time}秒，会话数据: {session_data}")
                session = session_data
            else:
                session = json.loads(session_data_str)
                logger.info(f"使用现有会话: {session_id}，已完成角度: {session.get('completed_angles', [])}")
            
            # 验证用户权限
            if session['user_id'] != user_id:
                raise Exception("无权访问此检测会话")
            
            # 获取图片信息
            image_url = image_info.get('imageUrl') or image_info.get('image_url')
            angle = image_info.get('angle')
            oss_path = image_info.get('ossPath') or image_info.get('oss_path')
            
            # 验证角度
            if angle not in required_angles:
                raise Exception(f"无效的拍摄角度: {angle}，支持的角度: {', '.join(required_angles)}")
            
            # 检查是否已经检测过此角度
            if angle in session['completed_angles']:
                raise Exception(f"角度 {angle} 已经检测过了")
            
            # 自动提取OSS路径（如果没有提供）
            if not oss_path:
                oss_path = self._extract_oss_path(image_url)
            
            # 转换角度为中文
            angle_cn = angle_mapping[angle]
            
            logger.info(f"开始检测{angle_cn}照片: {image_url}")
            
            # 调用AI API进行单张图片分析
            analysis_result = await self._call_ai_api_single_image(image_url, angle_cn)
            
            # 验证分析结果不为空
            if not analysis_result or analysis_result.strip() == "":
                logger.error(f"AI分析结果为空，角度: {angle_cn}, 图片: {image_url}")
                raise Exception(f"AI分析失败，{angle_cn}照片分析结果为空，请重试")
            
            logger.info(f"{angle_cn}照片AI分析完成，结果长度: {len(analysis_result)}")
            
            # 创建单张图片结果
            single_result = SingleImageResult(
                angle=angle,
                analysis=analysis_result,
                image_url=image_url,
                processed_at=datetime.now()
            )
            
            # 更新会话状态
            session['completed_angles'].append(angle)
            # 将SingleImageResult转换为字典以便JSON序列化
            result_dict = {
                'angle': single_result.angle,
                'analysis': single_result.analysis,
                'image_url': single_result.image_url,
                'processed_at': single_result.processed_at.isoformat()
            }
            session['results'].append(result_dict)
            session['images_info'].append({
                'image_url': image_url,
                'angle': angle,
                'oss_path': oss_path
            })
            
            # 保存更新后的会话到Redis
            logger.debug(f"保存会话到Redis: {session_key}，会话数据: {session}")
            await redis_manager.set(session_key, json.dumps(session), ex=self.session_expire_time)
            logger.info(f"会话已保存到Redis，当前已完成角度: {session['completed_angles']}")
            
            # 计算当前步骤
            current_step = len(session['completed_angles'])
            total_steps = len(required_angles)
            is_completed = current_step >= total_steps
            
            # 确定下一个需要的角度
            next_angle = None
            if not is_completed:
                for req_angle in required_angles:
                    if req_angle not in session['completed_angles']:
                        next_angle = req_angle
                        break
            
            logger.info(f"{angle_cn}照片检测完成，步骤: {current_step}/{total_steps}")
            
            return StepDetectionResponse(
                session_id=session_id,
                step=current_step,
                total_steps=total_steps,
                current_result=single_result,
                is_completed=is_completed,
                next_angle=next_angle
            )
            
        except Exception as e:
            logger.error(f"分步检测失败: {str(e)}")
            raise Exception(f"分步检测失败: {str(e)}")
    
    async def final_summary(self, user_id: int, session_id: str) -> FinalSummaryResponse:
        """最终汇总 - 生成综合检测结果"""
        try:
            logger.info(f"开始最终汇总 - 用户ID: {user_id}, 会话ID: {session_id}")
            
            # 验证会话
            session_key = f"{self.session_prefix}{session_id}"
            logger.debug(f"查找会话: {session_key}")
            session_data_str = await redis_manager.get(session_key)
            if not session_data_str:
                logger.warning(f"会话不存在或已过期: {session_id}")
                # 检查Redis中是否有其他相关会话
                all_sessions = await redis_manager.keys(f"{self.session_prefix}*")
                logger.info(f"当前Redis中的所有会话: {all_sessions}")
                raise Exception(f"会话不存在或已过期，请重新开始检测")
            
            session = json.loads(session_data_str)
            
            # 验证用户权限
            if session['user_id'] != user_id:
                raise Exception("无权访问此检测会话")
            
            # 验证是否完成所有步骤
            if len(session['completed_angles']) < 3:
                missing_angles = []
                required_angles = ['top', 'side', 'back']
                angle_mapping = {'top': '头顶面', 'side': '头侧面', 'back': '头后面'}
                
                for angle in required_angles:
                    if angle not in session['completed_angles']:
                        missing_angles.append(angle_mapping[angle])
                
                raise Exception(f"检测未完成，缺少角度: {', '.join(missing_angles)}")
            
            logger.info(f"开始最终汇总分析，会话: {session_id}")
            
            # 提取所有分析结果
            analysis_results = [result['analysis'] for result in session['results']]
            
            # 详细记录每个分析结果的内容
            logger.info(f"准备进行最终汇总，分析结果数量: {len(analysis_results)}")
            for i, result in enumerate(analysis_results):
                angle = session['results'][i].get('angle', f'未知角度{i+1}')
                logger.info(f"分析结果{i+1} (角度: {angle}) 长度: {len(result)}")
                logger.debug(f"分析结果{i+1} (角度: {angle}) 内容预览: {result[:200]}...")
            
            # 验证分析结果不为空
            empty_results = []
            invalid_results = []
            for i, result in enumerate(analysis_results):
                angle = session['results'][i].get('angle', f'未知角度{i+1}')
                if not result or result.strip() == "":
                    empty_results.append(f"结果{i+1}({angle})")
                elif len(result.strip()) < 50:  # 检查结果是否过短
                    invalid_results.append(f"结果{i+1}({angle})")
                    logger.warning(f"分析结果{i+1}({angle})内容过短: {result}")
            
            if empty_results:
                logger.error(f"发现空的分析结果: {', '.join(empty_results)}")
                logger.error(f"会话数据: {session}")
                raise Exception(f"检测数据不完整，{', '.join(empty_results)}为空，请重新进行检测")
            
            if invalid_results:
                logger.warning(f"发现可能无效的分析结果: {', '.join(invalid_results)}")
                # 不抛出异常，但记录警告
            
            # 调用汇总分析
            summary_response = await self._call_ai_api_summary(analysis_results)
            
            # 解析最终结果
            parsed_result = self._parse_ai_response(summary_response)
            logger.info(f"解析后的最终结果: {parsed_result}")
            
            # 检查图片有效性
            if not parsed_result.get("isValid", True):
                warning_msg = parsed_result.get("warning", "图片内容无效")
                logger.warning(f"图片内容无效: {warning_msg}")
                raise Exception(warning_msg)
            
            # 创建检测记录
            result_data = {
                "baspType": parsed_result.get("baspType", "未知"),
                "severity": parsed_result.get("severity", "未知"),
                "description": parsed_result.get("description", "检测完成"),
                "suggestion": parsed_result.get("suggestion", "请咨询专业医生"),
                "confidence": parsed_result.get("confidence", 0.0)
            }
            
            detection_record = await DetectionRecord.create(
                user_id=user_id,
                result=result_data,
                images_info=session['images_info']
            )
            
            logger.info(f"检测记录创建成功，记录ID: {detection_record.id}")
            
            # 检测完成后尝试打卡（仅限每日第一次检测）
            try:
                checkin_result = await CheckinService.perform_checkin(user_id)
                if checkin_result["success"]:
                    logger.info(f"用户 {user_id} 检测完成后自动打卡成功: {checkin_result['message']}")
                else:
                    logger.info(f"用户 {user_id} 检测完成后打卡状态: {checkin_result['message']}")
            except Exception as checkin_error:
                logger.error(f"检测完成后打卡失败: {str(checkin_error)}")
                # 打卡失败不影响检测结果返回
            
            # 构建图片信息列表
            images_info_list = []
            for img in session['images_info']:
                images_info_list.append(ImageInfo(
                    image_url=img['image_url'],
                    angle=img['angle'],
                    oss_path=img['oss_path']
                ))
            
            # 将字典格式的结果转换回SingleImageResult对象
            step_results = []
            for result_dict in session['results']:
                step_result = SingleImageResult(
                    angle=result_dict['angle'],
                    analysis=result_dict['analysis'],
                    image_url=result_dict['image_url'],
                    processed_at=datetime.fromisoformat(result_dict['processed_at'])
                )
                step_results.append(step_result)
            
            # 创建最终响应
            final_response = FinalSummaryResponse(
                session_id=session_id,
                record_id=detection_record.id,
                result=DetectionResult(
                    basp_type=parsed_result.get("baspType", "未知"),
                    severity=parsed_result.get("severity", "未知"),
                    description=parsed_result.get("description", "检测完成"),
                    suggestion=parsed_result.get("suggestion", "请咨询专业医生"),
                    confidence=parsed_result.get("confidence", 0.0),
                    is_valid=detection_record.is_valid
                ),
                images_info=images_info_list,
                step_results=step_results
            )
            
            # 清理会话（可选，也可以保留一段时间）
            # await redis_manager.delete(session_key)
            
            return final_response
            
        except Exception as e:
            logger.error(f"最终汇总失败: {str(e)}")
            raise Exception(f"最终汇总失败: {str(e)}")
    
    async def get_session_status(self, session_id: str) -> Optional[Dict[str, Any]]:
        """获取会话状态"""
        session_key = f"{self.session_prefix}{session_id}"
        session_data_str = await redis_manager.get(session_key)
        if not session_data_str:
            return None
        
        session = json.loads(session_data_str)
        return {
            'session_id': session_id,
            'user_id': session['user_id'],
            'created_at': session['created_at'],
            'completed_angles': session['completed_angles'],
            'total_steps': 3,
            'current_step': len(session['completed_angles']),
            'is_completed': len(session['completed_angles']) >= 3
        }
    
    async def cleanup_expired_sessions(self, expire_hours: int = 24):
        """清理过期会话（Redis会自动过期，此方法主要用于手动清理）"""
        # Redis会自动处理过期，这里可以实现额外的清理逻辑
        # 由于Redis的键会自动过期，通常不需要手动清理
        logger.info("Redis会话会自动过期，无需手动清理")
        return 0
    
    async def preprocess_images(self, image_urls: List[str]) -> bool:
        """图片预处理：检查图片是否为人头部照片
        
        Args:
            image_urls: 三张图片的URL列表
            
        Returns:
            bool: 所有图片都是人头部照片返回True，否则返回False
        """
        try:
            if len(image_urls) != 3:
                logger.warning(f"图片数量不正确，期望3张，实际{len(image_urls)}张")
                return False
            
            # 并发检查所有图片
            tasks = []
            for i, image_url in enumerate(image_urls):
                task = self._check_single_image_validity(image_url, f"image_{i+1}")
                tasks.append(task)
            
            # 等待所有检查完成
            results = await asyncio.gather(*tasks, return_exceptions=True)
            
            # 检查结果
            for i, result in enumerate(results):
                if isinstance(result, Exception):
                    logger.error(f"图片{i+1}检查失败: {str(result)}")
                    return False
                if not result:
                    logger.info(f"图片{i+1}不是有效的人头部照片")
                    return False
            
            logger.info("所有图片都是有效的人头部照片")
            return True
            
        except Exception as e:
            logger.error(f"图片预处理失败: {str(e)}")
            return False
    
    async def _check_single_image_validity(self, image_url: str, image_name: str) -> bool:
        """检查单张图片是否为人头部照片
        
        Args:
            image_url: 图片URL
            image_name: 图片名称（用于日志）
            
        Returns:
            bool: 是人头部照片返回True，否则返回False
        """
        max_retries = 2
        retry_delay = 1
        
        for attempt in range(max_retries):
            try:
                prompt = self._generate_image_validation_prompt()
                
                response = await self.openai_client.chat.completions.create(
                    model=self.ai_model,
                    messages=[
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "text",
                                    "text": prompt
                                },
                                {
                                    "type": "image_url",
                                    "image_url": {
                                        "url": image_url
                                    }
                                }
                            ]
                        }
                    ],
                    max_tokens=200,
                    temperature=0.1,
                    timeout=self.ai_timeout,
                    stream=True  # 启用流式输出
                )
                
                # 处理流式响应
                ai_response_content = ""
                async for chunk in response:
                    # 安全检查：最后一个chunk的choices字段可能为空列表
                    if chunk.choices and chunk.choices[0].delta.content is not None:
                        ai_response_content += chunk.choices[0].delta.content
                
                ai_response = ai_response_content.strip().lower()
                logger.info(f"{image_name}有效性检查AI响应: {ai_response}")
                
                # 判断AI响应是否表示图片有效
                # AI会返回"有效"或"无效"，我们检查关键词
                is_valid = "有效" in ai_response or "valid" in ai_response or "是" in ai_response
                
                return is_valid
                    
            except Exception as e:
                error_msg = str(e)
                
                # 检查是否为超时错误
                if "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
                    logger.warning(f"{image_name}AI检查超时 (尝试 {attempt + 1}/{max_retries}): {error_msg}")
                    if attempt < max_retries - 1:
                        await asyncio.sleep(retry_delay)
                        continue
                    else:
                        logger.error(f"{image_name}AI检查超时，已重试{max_retries}次")
                        return False
                
                # 其他错误
                else:
                    logger.error(f"{image_name}AI检查失败 (尝试 {attempt + 1}/{max_retries}): {error_msg}")
                    if attempt < max_retries - 1:
                        await asyncio.sleep(retry_delay)
                        continue
                    else:
                        return False
        
        return False
    
    def _generate_image_validation_prompt(self) -> str:
        """生成图片有效性检查的prompt"""
        return """
请检查这张图片是否是人的头部照片。

判断标准：
1. 图片中是否包含人的头部（头发、面部、头皮等）
2. 图片是否清晰可见
3. 图片是否适合用于头发分析

请简单回答：
- 如果是有效的人头部照片，请回答"有效"
- 如果不是人头部照片或图片不清晰，请回答"无效"

只需要回答"有效"或"无效"，不需要详细解释。
"""